package com.blank.shop.es;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.RandomUtils;
import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.cluster.metadata.AliasMetaData;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.InnerHitBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.metrics.Min;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.event.ContextStoppedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.IndexOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.document.SearchDocument;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.*;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author _blank
 */
@Slf4j
@SpringBootApplication
public class ElasticsearchApplication implements ApplicationRunner, CommandLineRunner, ApplicationContextAware {

    /*
     * 参考spring data es 官方文档: https://docs.spring.io/spring-data/elasticsearch/docs/4.0.9.RELEASE/reference/html/
     *
     * 参考spring data es example: https://github.com/spring-projects/spring-data-examples/tree/main/elasticsearch
     *
     * 参考rest-high-level-client: https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.6/_index_apis.html
     *
     * 参考elasticsearch官方文档: https://www.elastic.co/guide/en/elasticsearch/reference/7.6/rest-apis.html
     */

    @Resource
    private ElasticsearchRestTemplate elasticsearchTemplate;
    @Resource
    private RestHighLevelClient elasticsearchRestHighLevelClient;
    @Resource
    private ObjectMapper jacksonObjectMapper;
    @Setter
    private ApplicationContext applicationContext;

    @SuppressWarnings({"ConstantConditions"})
    @Override
    public void run(ApplicationArguments args) throws Exception {
        final IndexCoordinates coordinates = IndexCoordinates.of("index-spring-boot-starter-data-elasticsearch");
        final IndexOperations indexOperations = this.elasticsearchTemplate.indexOps(coordinates);

        // 演示api
        if (ArrayUtils.getLength(coordinates.getIndexNames()) > 1) {
            // 删除索引
            indexOperations.delete();
        }

        if (indexOperations.exists()) {
            log.info("索引{}已经存在！", coordinates.getIndexName());

            this.elasticsearchTemplate.execute(client -> {
                final Settings.Builder settings = Settings.builder()
                        .put("index.mapping.nested_fields.limit", "2048")
                        .put("index.mapping.nested_objects.limit", "163840")
                        .put("index.mapping.total_fields.limit", "8192")
                        .put("index.mapping.depth.limit", "32")
                        .put("index.highlight.max_analyzed_offset", 9999999)
                        // 非常耗es内存，危险
                        .put("index.max_result_window", Integer.MAX_VALUE)
                        .put("index.refresh_interval", "30s");

                final UpdateSettingsRequest updateSettingsRequest = new UpdateSettingsRequest(coordinates.getIndexName())
                        .settings(settings)
                        .timeout(TimeValue.timeValueHours(1L));
                return client.indices().putSettings(updateSettingsRequest, RequestOptions.DEFAULT);
            });
        } else {
            final Document settings = Document.create();
            settings.put("index.number_of_shards", 4);
            settings.put("index.number_of_replicas", 0);
            log.info("创建索引{}：{}", coordinates.getIndexName(), indexOperations.create(settings));
        }

        // 删除原有的别名
        final List<AliasMetaData> aliasMetaData = indexOperations.queryForAlias();
        if (!CollectionUtils.isEmpty(aliasMetaData)) {
            for (AliasMetaData aliasMetaDatum : aliasMetaData) {
                indexOperations.removeAlias(new AliasQuery(aliasMetaDatum.getAlias()));
            }
        }

        // 设置索引别名
        this.elasticsearchTemplate.execute(client -> {
            final IndicesAliasesRequest.AliasActions aliasActions = IndicesAliasesRequest.AliasActions
                    .add()
                    .index(coordinates.getIndexName())
                    .alias("zhangsan");
            final IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest()
                    .addAliasAction(aliasActions);
            return client.indices()
                    .updateAliases(indicesAliasesRequest, RequestOptions.DEFAULT)
                    .isAcknowledged();
        });
        indexOperations.addAlias(new AliasBuilder().withAliasName("lisi").build());
        indexOperations.addAlias(new AliasQuery("wangwu"));


        final int incAge = RandomUtils.nextInt(24, 48);
        final String appendName = RandomStringUtils.randomAlphanumeric(4);

        String source;
        // 插入一条数据
        source = this.jacksonObjectMapper.writeValueAsString(ImmutableMap.of("username", "张三" + appendName, "age", 18 + incAge));
        this.elasticsearchTemplate.index(new IndexQueryBuilder().withSource(source).build(), coordinates);

        final List<IndexQuery> queries = Lists.newArrayListWithCapacity(16);

        source = this.jacksonObjectMapper.writeValueAsString(ImmutableMap.of("username", "李四" + appendName, "age", 28 + incAge));
        queries.add(new IndexQueryBuilder().withSource(source).build());
        source = this.jacksonObjectMapper.writeValueAsString(ImmutableMap.of("username", "王五" + appendName, "age", 38 + incAge));
        queries.add(new IndexQueryBuilder().withSource(source).build());
        source = this.jacksonObjectMapper.writeValueAsString(ImmutableMap.of("username", "赵六" + appendName, "age", 48 + incAge));
        queries.add(new IndexQueryBuilder().withSource(source).build());
        source = this.jacksonObjectMapper.writeValueAsString(ImmutableMap.of("username", "孙七" + appendName, "age", 58 + incAge));
        queries.add(new IndexQueryBuilder().withSource(source).build());
        source = this.jacksonObjectMapper.writeValueAsString(ImmutableMap.of("username", "周八" + appendName, "age", 68 + incAge));
        queries.add(new IndexQueryBuilder().withSource(source).build());
        source = this.jacksonObjectMapper.writeValueAsString(ImmutableMap.of("username", "吴九" + appendName, "age", 78 + incAge));
        queries.add(new IndexQueryBuilder().withSource(source).build());
        source = this.jacksonObjectMapper.writeValueAsString(ImmutableMap.of("username", "郑十" + appendName, "age", 88 + incAge));
        queries.add(new IndexQueryBuilder().withSource(source).build());

        final List<String> bulkIndex = this.elasticsearchTemplate.bulkIndex(queries, coordinates);
        log.info("_bulk ids: {}", StringUtils.collectionToCommaDelimitedString(bulkIndex));


        // 构建查询条件
        final BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
                .must(QueryBuilders.rangeQuery("age").from(28).to(60))
                .must(QueryBuilders.matchPhraseQuery("username", "王"));

        // 构建es query
        final NativeSearchQuery query = new NativeSearchQueryBuilder()
                // 需要查询的字段
                .withSourceFilter(new FetchSourceFilterBuilder().withIncludes("username", "age").build())
                .withQuery(boolQuery)
                // 排序
                .withSort(SortBuilders.fieldSort("age").order(SortOrder.DESC))
                // 高亮所需对象
                .withHighlightBuilder(SearchSourceBuilder.highlight())
                // 需要高亮字段
                .withHighlightFields(new HighlightBuilder.Field("username"))
                // 分页
                .withPageable(PageRequest.of(0, 5))
                // 折叠相同字段值
                .withCollapseField("username.keyword")
                // aggs
                .addAggregation(AggregationBuilders.min("min_age").field("age"))
                .build();

        // 定制折叠特殊参数（可选）
        query.getCollapseBuilder().setInnerHits(new InnerHitBuilder("uk_key_name").setFrom(0).setSize(999));

        // see https://www.elastic.co/guide/en/elasticsearch/reference/7.6/search-request-body.html#request-body-search-track-total-hits
        query.setTrackTotalHits(true);

        log.debug("开始查询es数据！");
        final SearchHits<SearchDocument> search = this.elasticsearchTemplate.search(query, SearchDocument.class, coordinates);

        final long totalHits = search.getTotalHits();
        log.info("总命中数量：{}\n", totalHits);

        final Aggregations aggregations = search.getAggregations();
        final Min minAge = aggregations.get("min_age");
        log.info("min_age: {}", minAge.getValueAsString());

        final List<SearchHit<SearchDocument>> searchHits = search.getSearchHits();
        for (SearchHit<SearchDocument> searchHit : searchHits) {
            final String id = searchHit.getId();
            log.info("response id: {}", id);

            // 响应数据 map
            final SearchDocument content = searchHit.getContent();
            log.info("response data: {}", content);

            final List<Object> sortValues = searchHit.getSortValues();
            log.info("response fields: {}", StringUtils.collectionToCommaDelimitedString(sortValues));

            final List<String> hitHighlightField = searchHit.getHighlightField("username");
            log.info("response hitHighlightField: {}\n", StringUtils.collectionToCommaDelimitedString(hitHighlightField));

            // FIXME 未发现支持获取 org.elasticsearch.search.SearchHit.getInnerHits 的数据
        }

    }

    /**
     * ManagementFactory.getThreadMXBean()
     */
    @Override
    public void run(String... args) throws Exception {
        TimeUnit.MILLISECONDS.sleep(1500L);
        log.info("shutdown container...");
        if (this.applicationContext instanceof AbstractApplicationContext) {
            ((AbstractApplicationContext) this.applicationContext).stop();
            SpringApplication.exit(this.applicationContext);
        } else {
            // 这个close掉，容器就退出了
            this.elasticsearchRestHighLevelClient.close();
        }
    }

    /**
     * ((AbstractApplicationContext) this.applicationContext).stop();
     * 此方法被执行，ContextStoppedEvent事件才会被触发！
     *
     * @param event org.springframework.context.event.ContextStoppedEvent
     */
    @EventListener(classes = {ContextStoppedEvent.class})
    public void cse(ContextStoppedEvent event) {
        System.out.println("===============");
        System.out.println(event.getSource());
        System.out.println("===============");
    }

    public static void main(String[] args) throws Exception {
        SpringApplication.run(ElasticsearchApplication.class, args);
    }

}
